package org.etlt.expression.function;

import org.etlt.expression.IllegalExpressionException;
import org.etlt.expression.SettingReader;
import org.etlt.expression.VariableContext;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

public class FunctionFactory implements FunctionExecutor {

    private List<Object> innerFunctionInstances = new ArrayList<>();

    private List<Object> registeredFunctionObjects = new ArrayList<>();

    private FunctionSetting setting;

    public FunctionFactory() {
        innerFunctionInstances.add(new StringFunctions());
        innerFunctionInstances.add(new MathFunctions());
        innerFunctionInstances.add(new DateFunctions());
        innerFunctionInstances.add(new AggregationFunctions());
        innerFunctionInstances.add(new ListFunctions());
        innerFunctionInstances.add(new SetFunctions());
        initInnerFunctions();
        loadUserDefinedFunctions();
    }

    private static final String CONFIGURATION = "ud-functions.json";

    private final Map<String, FunctionInvoker> functionInvokerMap = new HashMap<String, FunctionInvoker>();

    @Override
    public Object invokeFunction(VariableContext context, String functionName, Object[] parameters) {
        FunctionInvoker functionInvoker = getFunction(functionName);
        return functionInvoker.invoke(context, parameters);
    }

    @Override
    public FunctionInvoker getFunction(String functionName) {
        String _functionName = toUpper(functionName);
        FunctionInvoker functionInvoker = this.functionInvokerMap.get(_functionName);
        if (functionInvoker == null) {
            throw new IllegalExpressionException("function not found: " + _functionName);
        }
        return functionInvoker;
    }

    protected void loadUserDefinedFunctions() {
        SettingReader reader = new SettingReader();
        InputStream udConfig = getClass().getClassLoader().getResourceAsStream(CONFIGURATION);
        try {
            if (udConfig != null) {
                this.setting = reader.read(udConfig, FunctionSetting.class);
                List<Class> udFunctions = this.setting.getFunctionByClass();
                udFunctions.forEach((clazz) -> {
                    if (clazz.isAssignableFrom(FunctionActor.class)) {
                        throw new IllegalExpressionException("user defined class is not a FunctionActor: " + clazz.getName());
                    }
                    try {
                        FunctionActor functionActor = (FunctionActor) clazz.getDeclaredConstructor(null).newInstance();
                        FunctionEnabled functionDesc = (FunctionEnabled) clazz.getAnnotation(FunctionEnabled.class);
                        if (functionDesc == null) {
                            throw new IllegalArgumentException("no FunctionEnabled annotation found: " + clazz.getName());
                        }
                        String functionName = toUpper(functionDesc.value());
                        checkExist(functionName);
                        FunctionInvoker invoker = new FunctionInvoker(functionActor);
                        invoker.setName(functionDesc.value());
                        invoker.setHelp(functionDesc.help());
                        this.functionInvokerMap.put(functionName, invoker);
                    } catch (Exception e) {
                        throw new IllegalExpressionException("user defined function initialization failed: " + clazz.getName(), e);
                    }
                });

            }
        }catch (IOException e){
            // do nothing
        }
    }

    protected void initInnerFunctions() {
        initFunctionObjects(this.innerFunctionInstances);
    }

    protected void initFunctionObjects(List<Object> functionObjects) {
        functionObjects.forEach(object ->{
            initFunctionObject(object);
        });
    }

    protected void initFunctionObject(Object instance) {
        Method[] methods = instance.getClass().getMethods();
        for (Method method : methods) {
            if (Modifier.isPublic(method.getModifiers())) {
                FunctionEnabled functionDesc = method.getAnnotation(FunctionEnabled.class);
                if (functionDesc != null) {
                    String functionName = toUpper(functionDesc.value());
                    checkExist(functionName);
                    FunctionInvoker invoker = new FunctionInvoker(method, instance);
                    invoker.setName(functionDesc.value());
                    invoker.setHelp(functionDesc.help());
                    this.functionInvokerMap.put(functionName, invoker);
                }
            }
        }
    }

    @Override
    public void registerFunctions(String type) {
        registerFunctionObject(type);
    }

    public void registerFunctionObject(Object object){
        this.registeredFunctionObjects.add(object);
        initFunctionObject(object);
    }

    public void registerFunctionObject(Class<?> clazz){
        try {
            Object object = clazz.newInstance();
            registerFunctionObject(object);
        }catch (Exception e){
            throw new IllegalStateException("register function object error.",e);
        }
    }

    public void registerFunctionObject(String type){
        try {
            Class<?> clazz = Class.forName(type);
            registerFunctionObject(clazz);
        }catch (Exception e){
            throw new IllegalStateException("register function object error.",e);
        }
    }

    /**
     * check if there is an existing function with this functionName
     *
     * @param functionName
     */
    protected void checkExist(String functionName) {
        String _functionName = toUpper(functionName);
        if (this.functionInvokerMap.containsKey(_functionName))
            throw new IllegalStateException("function exists: " + _functionName);
    }

    private String toUpper(String functionName) {
        return functionName.toUpperCase();
    }

    public List<FunctionInvoker> getAllFunctions() {
        return new ArrayList<>(this.functionInvokerMap.values());
    }
}
